Chomu's Blog.

>

Posts

GitHub

Promise.allArray.fromAsync

목차

개요

Array.fromAsync 가 ES2022에 추가되었을 때는 Promise.all과 동일한 작업을 하지만 Array.fromAsync가 좀더 사용하기 편하도록 만든 함수인 줄 알았는데 실제로 사용해보니 많이 달랐다. 두 함수의 공통점, 차이점과 각각의 사용법을 정리해둔다.

공통점

두 함수 모두 여러 Promise를 한 번에 처리할 수 있도록 도와준다.

Promise.all([Promise.resolve(1), Promise.resolve(2)]) satisfies Promise<number[]>;
Array.fromAsync([Promise.resolve(1), Promise.resolve(2)]) satisfies Promise<number[]>;

배열인 경우 타입으로만 보면 두 함수 모두 Array<Promise<T>>Promise<Array<T>> 형태로 껍데기를 벗겨주는(?) 역할을 한다.

차이점

1. 입력값

Promise.all의 타입 정의는 다음과 같이 되어 있다.

interface PromiseConstructor {
  // lib.es2015.promise.d.ts
  /**
   * Creates a Promise that is resolved with an array of results when all of the provided Promises
   * resolve, or rejected when any Promise is rejected.
   * @param values An array of Promises.
   * @returns A new Promise.
   */
  all<T extends readonly unknown[] | []>(values: T): Promise<{ -readonly [P in keyof T]: Awaited<T[P]>; }>;
 
  // lib.es2015.iterable.d.ts
  /**
   * Creates a Promise that is resolved with an array of results when all of the provided Promises
   * resolve, or rejected when any Promise is rejected.
   * @param values An iterable of Promises.
   * @returns A new Promise.
   */
  all<T>(values: Iterable<T | PromiseLike<T>>): Promise<Awaited<T>[]>;
}

두 타입 정의 모두 단 하나의 인자를 받는다.

Array.fromAsync는 다음과 같이 정의되어 있다.

interface ArrayConstructor {
    /**
     * Creates an array from an async iterator or iterable object.
     * @param iterableOrArrayLike An async iterator or array-like object to convert to an array.
     */
    fromAsync<T>(iterableOrArrayLike: AsyncIterable<T> | Iterable<T | PromiseLike<T>> | ArrayLike<T | PromiseLike<T>>): Promise<T[]>;
 
    /**
     * Creates an array from an async iterator or iterable object.
     *
     * @param iterableOrArrayLike An async iterator or array-like object to convert to an array.
     * @param mapfn A mapping function to call on every element of itarableOrArrayLike.
     *      Each return value is awaited before being added to result array.
     * @param thisArg Value of 'this' used when executing mapfn.
     */
    fromAsync<T, U>(iterableOrArrayLike: AsyncIterable<T> | Iterable<T> | ArrayLike<T>, mapFn: (value: Awaited<T>, index: number) => U, thisArg?: any): Promise<Awaited<U>[]>;
}

Array.fromAsync는 최대 두 개의 인자를 받는다. 두번째 인자 mapFnArray.from과 동일하게 각 요소를 변환하는 함수이다. 또 첫번째 인자도 AsyncIterable<T>ArrayLike<T | PromiseLike<T>> 를 받을 수 있어 커버리지가 더 넓다. 나는 여기까지만 보고 Array.fromAsyncPromise.all의 확장판인 줄 알았다. 하지만 실제로 사용해보니 두 함수의 작동방식이 많이 달랐다.

2. 작동방식

Promise.all은 입력값으로 받은 Promise병렬로 실행한다. 그에 비해 Array.fromAsync는 입력값으로 받은 Promise순차적으로 실행한다. 입력값이 Promise<T>[] 형태라면 이미 Promise들이 모두 실행된 상태이므로 두 함수의 동작 방식은 동일하다. 하지만 입력값이 Iterator<Promise<T>> 등 지연 평가 (lazy evaluation) 되는 경우라면 두 함수의 동작 방식이 달라진다.

const arr = [1, 2, 3];
const sec = () => new Date().getSeconds();
const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
const secWithDelay = (f: string) => async (i: number) => {
  await delay(1000);
  console.log(`function: ${f} item: ${i}, sec: ${sec()}`);
};
await Promise.all(Iterator.from(arr).map(secWithDelay("Promise.all")));
console.log("---");
await Array.fromAsync(Iterator.from(arr).map(secWithDelay("Array.fromAsync")));
/*
function: Promise.all item: 1, sec: 43
function: Promise.all item: 2, sec: 43
function: Promise.all item: 3, sec: 43
---
function: Array.fromAsync item: 1, sec: 44
function: Array.fromAsync item: 2, sec: 45
function: Array.fromAsync item: 3, sec: 46
*/

보다시피 Promise.all은 모든 Promise를 동시에 실행한 반면, Array.fromAsync는 각 Promise가 실행될 때마다 1초씩 지연이 발생했다. 좀더 이해하기 쉽게 두 함수를 명령형으로 직접 구현해보자면 다음과 비슷할 것이다.

async function promiseAll<T>(iter: Iterable<Promise<T>>) {
  const result = [];
  for (const item of Array.from(iter)) result.push(await item);
  return result;
}

그에 비해 Array.fromAsync는 다음과 같이 구현될 것이다.

async function arrayFromAsync<T>(iter: Iterable<Promise<T>>) {
  const result = [];
  for (const item of iter) result.push(await item);
  return result;
}

Promise.allArray.from(iter)를 통해 모든 Promise를 미리 평가한 반면, Array.fromAsynciter를 순차적으로 순회하면서 각 Promise를 평가한다.

3. 사용법

이런 차이로 인해 Array.fromAsync는 입력값으로 받은 Promise가 순차적으로 실행되므로, Promise의 실행 순서가 중요할 때 유용하게 사용할 수 있다. 반면 Promise.all은 모든 Promise를 병렬로 실행하므로, 병렬적으로 모든 Promise를 빠르게 처리하고 싶을 때 유용하다.

예시

예를 들어 SNS의 타임라인을 불러오는 경우를 생각해보자. 글은 각각 순서대로 불러와야 하지만, 각 글의 리소스(텍스트, 이미지 등)은 순서가 중요하지 않으므로 병렬로 빠르게 처리하는 것이 좋을 것이다.

const resourceUris = Iterator.from([
  Iterator.from([
    "https://example.com/post/1/resource/1",
    "https://example.com/post/1/resource/2",
  ]),
  Iterator.from([
    "https://example.com/post/2/resource/1",
    "https://example.com/post/2/resource/2",
  ]),
  ...
]);
 
function fetchPosts(resourceUris: string[][]) {
  return Array.fromAsync(resourceUris.map(
    (uris) => Promise.all(uris.map(fetch))
  ));
}

이렇게 하면 각 글의 리소스는 Promise.all 를 통해 병렬로 빠르게 불러오면서도, Array.fromAsync를 통해 글은 순서대로 불러올 수 있다. 이런 식으로 Array.fromAsyncPromise.all을 적절히 조합하여 사용할 수 있다.

결론

Promise.allArray.fromAsync는 모두 여러 Promise를 처리할 수 있도록 도와주는 함수이지만, 입력값의 형태와 작동 방식이 다르다. 단순 배열이라면 큰 상관 없겠지만 Iterator를 사용한다면 두 함수의 차이를 이해하고 적절히 사용해야 한다. 동시 병렬 처리가 필요한 경우 Promise.all, 순차적으로 처리해야 하는 경우 Array.fromAsync를 사용해야 한다.